# Copyright (c) 2008
#   Matt Pizzimenti (mjpizz@gmail.com, www.livelearncode.com)
#
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# 
# Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 
# Neither the name of the original author nor the names of contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import sys
import os
import exceptions
from inspect import getargspec

import utils
import interaction
from tossfiles import TossfileManager as _TossfileManager
from receipts import ReceiptManager as _ReceiptManager
from events import RollbackBeganEvent
from events import RollbackFinishedEvent
from events import ErrorEvent
from events import FatalEvent


class TaskExecutionError(exceptions.Exception): pass


class Task(object):
    
    kwargs_descriptions = property(lambda self: self.__kwargs_descriptions)
    sorted_kwargs = property(lambda self: self.__sorted_kwargs)

    def __init__(self, name, callback):
        self.name = name
        self.callback = callback
        self.description = self.callback.__doc__

    def execute(self):
        if TaskManager.get_instance().has_execution_stack():

            try:
                TaskManager.get_instance().about_to_execute_task(self)
                self.callback()
                TaskManager.get_instance().finished_executing_task(self)
            except:
                TaskManager.get_instance().encountered_exception_in_task(self, sys.exc_info())
                raise TaskExecutionError("exception occurred in Task '%s' (%s)" % (self.name, sys.exc_info()[1]))
                
        else:
            TaskManager.get_instance().begin_execution_stack(self)


class TaskManager(object):

    def __init__(self):
        # TODO: private vars

        # initialize the stacks/lists we use for running a set of tasks
        self.running_task_stack = None
        self.rollback_stack = None
        self.completed_task_list = None
        self.stack_exception_handled = False

        # task tracking
        self.task_lookup = {}
        self.ordered_tasks = []
        
    @classmethod
    def get_instance(klass):
        try:
            instance = klass.instance
        except:
            klass.instance = None
        if not klass.instance:
            klass.instance = klass()
        return klass.instance

    def len_of_longest_task_name(self):
        if len(self.task_lookup) == 0:
            return 0
        else:
            return max([len(key) for key in self.task_lookup])

    def register_rollback(self, callback):
        rollback_entry = RollbackEntry(self.currently_active_task(), callback)
        self.rollback_stack.append(rollback_entry)

    def begin_execution_stack(self, task, **kwargs):
        self.running_task_stack = []
        self.completed_task_list = []
        self.rollback_stack = []
        self.stack_exception_handled = False
        self.current_rollback_task = None
        
        def finish_execution():
            self.running_task_stack = None
            self.completed_task_list = None
            self.rollback_stack = None
            self.current_rollback_task = None
            
        try:
            task.execute(**kwargs)
        except:
            finish_execution()
            raise
        finish_execution()

    def has_execution_stack(self):
        return self.running_task_stack != None

    def about_to_execute_task(self, task):
        if self.has_execution_stack():
            self.running_task_stack.append(task)
        else:
            raise TaskExecutionError("tried to execute a task without an task execution stack")

    def finished_executing_task(self, task):
        popped_task = self.running_task_stack.pop()
        self.completed_task_list.append(popped_task)
        if popped_task != task:
            raise TaskExecutionError("task execution stack corruption detected (expected '%s' to be popped, but instead it was '%s')" % (task.name, popped_task.name))

    def currently_active_task(self):
        """
        returns the task that is currently either:
        (a) running
        or
        (b) rolling back
        """
        if self.running_task_stack and len(self.running_task_stack) > 0:
            if self.current_rollback_task:
                # we are rolling back, so show the current rollback task
                return self.current_rollback_task
            else:
                # we are executing, so show the currently executing task
                return self.running_task_stack[-1]
        else:
            # no run-stack, so no active task
            return None

    def get_all_tasks(self):
        """
        returns a list of all tasks in the order they were declared
        """
        return self.ordered_tasks

    def create_task(self, task_callback):
        """creates a Task, properly namespacing it for commandline
        selection and execution"""
        
        # determine the "namespace" of the task callback (uses the directory structure on disk)
        task_namespace = self._determine_namespace_of_object(task_callback)
        
        # create the Task, and add it to the task lookup
        if len(task_namespace) > 0:
            task_name = "%s%s" % (task_namespace, task_callback.__name__)
        else:
            task_name = str(task_callback.__name__)
        
        task = Task(name = task_name, callback = task_callback)

        self.task_lookup[task_name] = task
        self.ordered_tasks.append(task)
        
        # return the Task
        return task
    
    def _determine_namespace_of_object(self, the_object):
        
        # figure out where this object is coming from (on disk)
        absolute_object_path = utils.absolute_directory_of_object( the_object )
        absolute_tossfile_path = _TossfileManager.get_instance().main_tossfile_absolute_path
        if not absolute_tossfile_path:
            # bugfix when main_tossfile_absolute_path isn't set (when you import module from other python scripts)
            absolute_tossfile_path = os.getcwd()
        relative_object_path = utils.relative_path_between(absolute_tossfile_path, absolute_object_path)
        
        if relative_object_path.startswith("tossfile") or len(relative_object_path) == 0:

            # this is a "root" task (from the main tossfile module)
            module_md5_identifier = the_object.__module__
            if _TossfileManager.get_instance().is_main_tossfile_module( module_md5_identifier ):
                
                # this was in tossfile.py or tossfile/__init__.py, no namespace
                object_namespace = ""
                
            else:
                
                # otherwise, this was a submodule of tossfile, so give it a namespace
                object_namespace = str( the_object.__module__ ) + "."
            
        else:
            
            # in this case, we have a completely external Tossfile reference, use the
            # relative path to describe the task namespace
            # NOTE: we used to convert slashes to dots here, but it is too error-prone
            object_namespace = "%s%s" % (relative_object_path, utils.os_path_separator)
                
        return object_namespace

    def find_task(self, task_name):
        return self.task_lookup.get(task_name, None)

    def encountered_exception_in_task(self, task, exc_info):
        """callback for Tasks to use that will trigger a rollback
        on the Task execution stack"""
        if not self.stack_exception_handled:
            _ReceiptManager.get_instance().log_event( ErrorEvent( sys.exc_info() ) )
            try:
                if len(self.rollback_stack) > 0:
                    _ReceiptManager.get_instance().log_event( RollbackBeganEvent() )
                    while len(self.rollback_stack) > 0:
                        rollback_entry = self.rollback_stack.pop()
                        self.current_rollback_task = rollback_entry.task
                        rollback_entry.rollback()
                    self.current_rollback_task = None
                    _ReceiptManager.get_instance().log_event( RollbackFinishedEvent() )
            except:
                self.finished_executing_task(task)
                self.stack_exception_handled = True
                raise
            self.finished_executing_task(task)
            self.stack_exception_handled = True


class RollbackEntry(object):

    def __init__(self, task, callback):
        # TODO: private vars
        self.task = task
        self.callback = callback

    def rollback(self):
        try:
            self.callback()
        except:
            fatal_event = FatalEvent( sys.exc_info() )
            _ReceiptManager.get_instance().log_event( fatal_event )
            raise
